/*
 * ProGuard -- shrinking, optimization, obfuscation, and preverification of Java bytecode.
 *
 * Copyright (c) 2002-2018 GuardSquare NV
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with this program; if not, write to the Free
 * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */
package proguard;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;

import proguard.classfile.ClassConstants;
import proguard.classfile.ClassPool;
import proguard.classfile.attribute.annotation.visitor.AllElementValueVisitor;
import proguard.classfile.attribute.visitor.AllAttributeVisitor;
import proguard.classfile.constant.visitor.AllConstantVisitor;
import proguard.classfile.instruction.visitor.AllInstructionVisitor;
import proguard.classfile.util.ClassReferenceInitializer;
import proguard.classfile.util.ClassSubHierarchyInitializer;
import proguard.classfile.util.ClassSuperHierarchyInitializer;
import proguard.classfile.util.DynamicClassReferenceInitializer;
import proguard.classfile.util.DynamicMemberReferenceInitializer;
import proguard.classfile.util.EnumFieldReferenceInitializer;
import proguard.classfile.util.StringReferenceInitializer;
import proguard.classfile.util.StringSharer;
import proguard.classfile.util.WarningPrinter;
import proguard.classfile.visitor.AllMethodVisitor;
import proguard.classfile.visitor.ClassHierarchyTraveler;
import proguard.classfile.visitor.ClassNameFilter;
import proguard.classfile.visitor.ClassPoolFiller;
import proguard.classfile.visitor.LibraryClassFilter;
import proguard.classfile.visitor.MultiClassVisitor;
import proguard.classfile.visitor.ReferencedClassVisitor;
import proguard.util.ClassNameParser;
import proguard.util.EmptyStringMatcher;
import proguard.util.ListParser;
import proguard.util.NameParser;
import proguard.util.StringMatcher;

/**
 * This class initializes class pools and resource information.
 *
 * @author Eric Lafortune
 */
public class Initializer {
    private final Configuration configuration;

    /**
     * Creates a new Initializer to initialize classes according to the given configuration.
     */
    public Initializer(Configuration configuration) {
        this.configuration = configuration;
    }

    /**
     * Initializes the classes in the given program class pool and library class pool, performs some basic checks, and
     * shrinks the library class pool.
     */
    public void execute(ClassPool programClassPool, ClassPool libraryClassPool) throws IOException {
        // We're using the system's default character encoding for writing to
        // the standard output and error output.
        PrintWriter out = new PrintWriter(System.out, true);
        PrintWriter err = new PrintWriter(System.err, true);

        int originalLibraryClassPoolSize = libraryClassPool.size();

        // Perform basic checks on the configuration.
        WarningPrinter fullyQualifiedClassNameNotePrinter = new WarningPrinter(System.out, configuration.note);

        FullyQualifiedClassNameChecker fullyQualifiedClassNameChecker =
            new FullyQualifiedClassNameChecker(programClassPool, libraryClassPool, fullyQualifiedClassNameNotePrinter);

        fullyQualifiedClassNameChecker.checkClassSpecifications(configuration.keep);
        fullyQualifiedClassNameChecker.checkClassSpecifications(configuration.assumeNoSideEffects);
        fullyQualifiedClassNameChecker.checkClassSpecifications(configuration.assumeNoExternalSideEffects);
        fullyQualifiedClassNameChecker.checkClassSpecifications(configuration.assumeNoEscapingParameters);
        fullyQualifiedClassNameChecker.checkClassSpecifications(configuration.assumeNoExternalReturnValues);

        StringMatcher keepAttributesMatcher = configuration.keepAttributes != null
            ? new ListParser(new NameParser()).parse(configuration.keepAttributes) : new EmptyStringMatcher();

        WarningPrinter getAnnotationNotePrinter = new WarningPrinter(System.out, configuration.note);

        if (!keepAttributesMatcher.matches(ClassConstants.ATTR_RuntimeVisibleAnnotations)) {
            programClassPool.classesAccept(new AllConstantVisitor(new GetAnnotationChecker(getAnnotationNotePrinter)));
        }

        WarningPrinter getSignatureNotePrinter = new WarningPrinter(System.out, configuration.note);

        if (!keepAttributesMatcher.matches(ClassConstants.ATTR_Signature)) {
            programClassPool.classesAccept(new AllConstantVisitor(new GetSignatureChecker(getSignatureNotePrinter)));
        }

        WarningPrinter getEnclosingClassNotePrinter = new WarningPrinter(System.out, configuration.note);

        if (!keepAttributesMatcher.matches(ClassConstants.ATTR_InnerClasses)) {
            programClassPool
                .classesAccept(new AllConstantVisitor(new GetEnclosingClassChecker(getEnclosingClassNotePrinter)));
        }

        WarningPrinter getEnclosingMethodNotePrinter = new WarningPrinter(System.out, configuration.note);

        if (!keepAttributesMatcher.matches(ClassConstants.ATTR_EnclosingMethod)) {
            programClassPool
                .classesAccept(new AllConstantVisitor(new GetEnclosingMethodChecker(getEnclosingMethodNotePrinter)));
        }

        // Construct a reduced library class pool with only those library
        // classes whose hierarchies are referenced by the program classes.
        // We can't do this if we later have to come up with the obfuscated
        // class member names that are globally unique.
        ClassPool reducedLibraryClassPool = configuration.useUniqueClassMemberNames ? null : new ClassPool();

        WarningPrinter classReferenceWarningPrinter = new WarningPrinter(System.err, configuration.warn);
        WarningPrinter dependencyWarningPrinter = new WarningPrinter(System.err, configuration.warn);

        // Initialize the superclass hierarchies for program classes.
        programClassPool.classesAccept(
            new ClassSuperHierarchyInitializer(programClassPool, libraryClassPool, classReferenceWarningPrinter, null));

        // Initialize the superclass hierarchy of all library classes, without
        // warnings.
        libraryClassPool.classesAccept(
            new ClassSuperHierarchyInitializer(programClassPool, libraryClassPool, null, dependencyWarningPrinter));

        // Initialize the class references of program class members and
        // attributes. Note that all superclass hierarchies have to be
        // initialized for this purpose.
        WarningPrinter programMemberReferenceWarningPrinter = new WarningPrinter(System.err, configuration.warn);
        WarningPrinter libraryMemberReferenceWarningPrinter = new WarningPrinter(System.err, configuration.warn);

        programClassPool.classesAccept(
            new ClassReferenceInitializer(programClassPool, libraryClassPool, classReferenceWarningPrinter,
                programMemberReferenceWarningPrinter, libraryMemberReferenceWarningPrinter, null));

        if (reducedLibraryClassPool != null) {
            // Collect the library classes that are directly referenced by
            // program classes, without introspection.
            programClassPool.classesAccept(
                new ReferencedClassVisitor(new LibraryClassFilter(new ClassPoolFiller(reducedLibraryClassPool))));

            // Reinitialize the superclass hierarchies of referenced library
            // classes, this time with warnings.
            reducedLibraryClassPool.classesAccept(new ClassSuperHierarchyInitializer(programClassPool, libraryClassPool,
                classReferenceWarningPrinter, null));
        }

        // Initialize the enum annotation references.
        programClassPool.classesAccept(
            new AllAttributeVisitor(true, new AllElementValueVisitor(true, new EnumFieldReferenceInitializer())));

        // Initialize the Class.forName references.
        WarningPrinter dynamicClassReferenceNotePrinter = new WarningPrinter(System.out, configuration.note);
        WarningPrinter classForNameNotePrinter = new WarningPrinter(System.out, configuration.note);

        programClassPool.classesAccept(new AllMethodVisitor(new AllAttributeVisitor(new AllInstructionVisitor(
            new DynamicClassReferenceInitializer(programClassPool, libraryClassPool, dynamicClassReferenceNotePrinter,
                null, classForNameNotePrinter, createClassNoteExceptionMatcher(configuration.keep, true))))));

        // Initialize the WebView.addJavascriptInterface references.
        WarningPrinter webViewClassReferenceNotePrinter = new WarningPrinter(System.out, configuration.note);

        // Initialize the Class.get[Declared]{Field,Method} references.
        WarningPrinter getMemberNotePrinter = new WarningPrinter(System.out, configuration.note);

        programClassPool.classesAccept(
            new AllMethodVisitor(new AllAttributeVisitor(new DynamicMemberReferenceInitializer(programClassPool,
                libraryClassPool, getMemberNotePrinter, createClassMemberNoteExceptionMatcher(configuration.keep, true),
                createClassMemberNoteExceptionMatcher(configuration.keep, false)))));

        // Initialize other string constant references, if requested.
        if (configuration.adaptClassStrings != null) {
            programClassPool.classesAccept(new ClassNameFilter(configuration.adaptClassStrings,
                new AllConstantVisitor(new StringReferenceInitializer(programClassPool, libraryClassPool))));
        }

        // Initialize the class references of library class members.
        if (reducedLibraryClassPool != null) {
            // Collect the library classes that are referenced by program
            // classes, directly or indirectly, with or without reflection.
            programClassPool
                .classesAccept(new ReferencedClassVisitor(new LibraryClassFilter(new ClassHierarchyTraveler(true, true,
                    true, false, new LibraryClassFilter(new ClassPoolFiller(reducedLibraryClassPool))))));

            // Initialize the class references of referenced library
            // classes, without warnings.
            reducedLibraryClassPool.classesAccept(new ClassReferenceInitializer(programClassPool, libraryClassPool,
                null, null, null, dependencyWarningPrinter));

            // Reset the library class pool.
            libraryClassPool.clear();

            // Copy the library classes that are referenced directly by program
            // classes and the library classes that are referenced by referenced
            // library classes.
            reducedLibraryClassPool.classesAccept(new MultiClassVisitor(
                new ClassHierarchyTraveler(true, true, true, false,
                    new LibraryClassFilter(new ClassPoolFiller(libraryClassPool))),

                new ReferencedClassVisitor(new LibraryClassFilter(new ClassHierarchyTraveler(true, true, true, false,
                    new LibraryClassFilter(new ClassPoolFiller(libraryClassPool)))))));
        } else {
            // Initialize the class references of all library class members.
            libraryClassPool.classesAccept(new ClassReferenceInitializer(programClassPool, libraryClassPool, null, null,
                null, dependencyWarningPrinter));
        }

        // Initialize the subclass hierarchies.
        programClassPool.classesAccept(new ClassSubHierarchyInitializer());
        libraryClassPool.classesAccept(new ClassSubHierarchyInitializer());

        // Share strings between the classes, to reduce heap memory usage.
        programClassPool.classesAccept(new StringSharer());
        libraryClassPool.classesAccept(new StringSharer());

        // Check for any unmatched class members.
        WarningPrinter classMemberNotePrinter = new WarningPrinter(System.out, configuration.note);

        ClassMemberChecker classMemberChecker = new ClassMemberChecker(programClassPool, classMemberNotePrinter);

        classMemberChecker.checkClassSpecifications(configuration.keep);
        classMemberChecker.checkClassSpecifications(configuration.assumeNoSideEffects);
        classMemberChecker.checkClassSpecifications(configuration.assumeNoExternalSideEffects);
        classMemberChecker.checkClassSpecifications(configuration.assumeNoEscapingParameters);
        classMemberChecker.checkClassSpecifications(configuration.assumeNoExternalReturnValues);

        // Check for unkept descriptor classes of kept class members.
        WarningPrinter descriptorKeepNotePrinter = new WarningPrinter(System.out, configuration.note);

        new DescriptorKeepChecker(programClassPool, libraryClassPool, descriptorKeepNotePrinter)
            .checkClassSpecifications(configuration.keep);

        // Check for keep options that only match library classes.
        WarningPrinter libraryKeepNotePrinter = new WarningPrinter(System.out, configuration.note);

        new LibraryKeepChecker(programClassPool, libraryClassPool, libraryKeepNotePrinter)
            .checkClassSpecifications(configuration.keep);

        // Print out a summary of the notes, if necessary.
        int fullyQualifiedNoteCount = fullyQualifiedClassNameNotePrinter.getWarningCount();
        if (fullyQualifiedNoteCount > 0) {
            System.out.println("Note: there were " + fullyQualifiedNoteCount + " references to unknown classes.");
            System.out.println("      You should check your configuration for typos.");
            System.out.println("      (http://proguard.sourceforge.net/manual/troubleshooting.html#unknownclass)");
        }

        int classMemberNoteCount = classMemberNotePrinter.getWarningCount();
        if (classMemberNoteCount > 0) {
            System.out.println("Note: there were " + classMemberNoteCount + " references to unknown class members.");
            System.out.println("      You should check your configuration for typos.");
        }

        int getAnnotationNoteCount = getAnnotationNotePrinter.getWarningCount();
        if (getAnnotationNoteCount > 0) {
            System.out.println("Note: there were " + getAnnotationNoteCount
                + " classes trying to access annotations using reflection.");
            System.out.println("      You should consider keeping the annotation attributes");
            System.out.println("      (using '-keepattributes *Annotation*').");
            System.out.println("      (http://proguard.sourceforge.net/manual/troubleshooting.html#attributes)");
        }

        int getSignatureNoteCount = getSignatureNotePrinter.getWarningCount();
        if (getSignatureNoteCount > 0) {
            System.out.println("Note: there were " + getSignatureNoteCount
                + " classes trying to access generic signatures using reflection.");
            System.out.println("      You should consider keeping the signature attributes");
            System.out.println("      (using '-keepattributes Signature').");
            System.out.println("      (http://proguard.sourceforge.net/manual/troubleshooting.html#attributes)");
        }

        int getEnclosingClassNoteCount = getEnclosingClassNotePrinter.getWarningCount();
        if (getEnclosingClassNoteCount > 0) {
            System.out.println("Note: there were " + getEnclosingClassNoteCount
                + " classes trying to access enclosing classes using reflection.");
            System.out.println("      You should consider keeping the inner classes attributes");
            System.out.println("      (using '-keepattributes InnerClasses').");
            System.out.println("      (http://proguard.sourceforge.net/manual/troubleshooting.html#attributes)");
        }

        int getEnclosingMethodNoteCount = getEnclosingMethodNotePrinter.getWarningCount();
        if (getEnclosingMethodNoteCount > 0) {
            System.out.println("Note: there were " + getEnclosingMethodNoteCount
                + " classes trying to access enclosing methods using reflection.");
            System.out.println("      You should consider keeping the enclosing method attributes");
            System.out.println("      (using '-keepattributes InnerClasses,EnclosingMethod').");
            System.out.println("      (http://proguard.sourceforge.net/manual/troubleshooting.html#attributes)");
        }

        int descriptorNoteCount = descriptorKeepNotePrinter.getWarningCount();
        if (descriptorNoteCount > 0) {
            System.out.println(
                "Note: there were " + descriptorNoteCount + " unkept descriptor classes in kept class members.");
            System.out.println("      You should consider explicitly keeping the mentioned classes");
            System.out.println("      (using '-keep').");
            System.out.println("      (http://proguard.sourceforge.net/manual/troubleshooting.html#descriptorclass)");
        }

        int libraryNoteCount = libraryKeepNotePrinter.getWarningCount();
        if (libraryNoteCount > 0) {
            System.out.println("Note: there were " + libraryNoteCount + " library classes explicitly being kept.");
            System.out.println("      You don't need to keep library classes; they are already left unchanged.");
            System.out.println("      (http://proguard.sourceforge.net/manual/troubleshooting.html#libraryclass)");
        }

        int dynamicClassReferenceNoteCount = dynamicClassReferenceNotePrinter.getWarningCount();
        if (dynamicClassReferenceNoteCount > 0) {
            System.out.println("Note: there were " + dynamicClassReferenceNoteCount
                + " unresolved dynamic references to classes or interfaces.");
            System.out.println("      You should check if you need to specify additional program jars.");
            System.out.println("      (http://proguard.sourceforge.net/manual/troubleshooting.html#dynamicalclass)");
        }

        int classForNameNoteCount = classForNameNotePrinter.getWarningCount();
        if (classForNameNoteCount > 0) {
            System.out.println(
                "Note: there were " + classForNameNoteCount + " class casts of dynamically created class instances.");
            System.out.println("      You might consider explicitly keeping the mentioned classes and/or");
            System.out.println("      their implementations (using '-keep').");
            System.out
                .println("      (http://proguard.sourceforge.net/manual/troubleshooting.html#dynamicalclasscast)");
        }

        int getmemberNoteCount = getMemberNotePrinter.getWarningCount();
        if (getmemberNoteCount > 0) {
            System.out.println(
                "Note: there were " + getmemberNoteCount + " accesses to class members by means of introspection.");
            System.out.println("      You should consider explicitly keeping the mentioned class members");
            System.out.println("      (using '-keep' or '-keepclassmembers').");
            System.out
                .println("      (http://proguard.sourceforge.net/manual/troubleshooting.html#dynamicalclassmember)");
        }

        // Print out a summary of the warnings, if necessary.
        int classReferenceWarningCount = classReferenceWarningPrinter.getWarningCount();
        if (classReferenceWarningCount > 0) {
            System.err.println("Warning: there were " + classReferenceWarningCount
                + " unresolved references to classes or interfaces.");
            System.err.println("         You may need to add missing library jars or update their versions.");
            System.err.println("         If your code works fine without the missing classes, you can suppress");
            System.err.println("         the warnings with '-dontwarn' options.");

            if (configuration.skipNonPublicLibraryClasses) {
                System.err.println("         You may also have to remove the option '-skipnonpubliclibraryclasses'.");
            }

            System.err
                .println("         (http://proguard.sourceforge.net/manual/troubleshooting.html#unresolvedclass)");
        }

        int dependencyWarningCount = dependencyWarningPrinter.getWarningCount();
        if (dependencyWarningCount > 0) {
            System.err.println("Warning: there were " + dependencyWarningCount
                + " instances of library classes depending on program classes.");
            System.err.println("         You must avoid such dependencies, since the program classes will");
            System.err.println("         be processed, while the library classes will remain unchanged.");
            System.err.println("         (http://proguard.sourceforge.net/manual/troubleshooting.html#dependency)");
        }

        int programMemberReferenceWarningCount = programMemberReferenceWarningPrinter.getWarningCount();
        if (programMemberReferenceWarningCount > 0) {
            System.err.println("Warning: there were " + programMemberReferenceWarningCount
                + " unresolved references to program class members.");
            System.err.println("         Your input classes appear to be inconsistent.");
            System.err.println("         You may need to recompile the code.");
            System.err.println(
                "         (http://proguard.sourceforge.net/manual/troubleshooting.html#unresolvedprogramclassmember)");
        }

        int libraryMemberReferenceWarningCount = libraryMemberReferenceWarningPrinter.getWarningCount();
        if (libraryMemberReferenceWarningCount > 0) {
            System.err.println("Warning: there were " + libraryMemberReferenceWarningCount
                + " unresolved references to library class members.");
            System.err.println("         You probably need to update the library versions.");

            if (!configuration.skipNonPublicLibraryClassMembers) {
                System.err.println("         Alternatively, you may have to specify the option ");
                System.err.println("         '-dontskipnonpubliclibraryclassmembers'.");
            }

            if (configuration.skipNonPublicLibraryClasses) {
                System.err.println("         You may also have to remove the option '-skipnonpubliclibraryclasses'.");
            }

            System.err.println(
                "         (http://proguard.sourceforge.net/manual/troubleshooting.html#unresolvedlibraryclassmember)");
        }

        if ((classReferenceWarningCount > 0 || dependencyWarningCount > 0 || programMemberReferenceWarningCount > 0
            || libraryMemberReferenceWarningCount > 0) && !configuration.ignoreWarnings) {
            throw new IOException("Please correct the above warnings first.");
        }

        if ((configuration.note == null || !configuration.note.isEmpty())
            && (configuration.warn != null && configuration.warn.isEmpty() || configuration.ignoreWarnings)) {
            System.out.println("Note: you're ignoring all warnings!");
        }

        // Discard unused library classes.
        if (configuration.verbose) {
            System.out.println("Ignoring unused library classes...");
            System.out.println("  Original number of library classes: " + originalLibraryClassPoolSize);
            System.out.println("  Final number of library classes:    " + libraryClassPool.size());
        }
    }

    /**
     * Extracts a list of exceptions of classes for which not to print notes, from the keep configuration.
     */
    private StringMatcher createClassNoteExceptionMatcher(List noteExceptions, boolean markClasses) {
        if (noteExceptions != null) {
            List noteExceptionNames = new ArrayList(noteExceptions.size());
            for (int index = 0; index < noteExceptions.size(); index++) {
                KeepClassSpecification keepClassSpecification = (KeepClassSpecification)noteExceptions.get(index);
                if (keepClassSpecification.markClasses || !markClasses) {
                    // If the class itself is being kept, it's ok.
                    String className = keepClassSpecification.className;
                    if (className != null && !containsWildCardReferences(className)) {
                        noteExceptionNames.add(className);
                    }

                    // If all of its extensions are being kept, it's ok too.
                    String extendsClassName = keepClassSpecification.extendsClassName;
                    if (extendsClassName != null && !containsWildCardReferences(extendsClassName)) {
                        noteExceptionNames.add(extendsClassName);
                    }
                }
            }

            if (noteExceptionNames.size() > 0) {
                return new ListParser(new ClassNameParser()).parse(noteExceptionNames);
            }
        }

        return null;
    }

    /**
     * Extracts a list of exceptions of field or method names for which not to print notes, from the keep configuration.
     */
    private StringMatcher createClassMemberNoteExceptionMatcher(List noteExceptions, boolean isField) {
        if (noteExceptions != null) {
            List noteExceptionNames = new ArrayList();
            for (int index = 0; index < noteExceptions.size(); index++) {
                KeepClassSpecification keepClassSpecification = (KeepClassSpecification)noteExceptions.get(index);
                List memberSpecifications =
                    isField ? keepClassSpecification.fieldSpecifications : keepClassSpecification.methodSpecifications;

                if (memberSpecifications != null) {
                    for (int index2 = 0; index2 < memberSpecifications.size(); index2++) {
                        MemberSpecification memberSpecification = (MemberSpecification)memberSpecifications.get(index2);

                        String memberName = memberSpecification.name;
                        if (memberName != null && !containsWildCardReferences(memberName)) {
                            noteExceptionNames.add(memberName);
                        }
                    }
                }
            }

            if (noteExceptionNames.size() > 0) {
                return new ListParser(new NameParser()).parse(noteExceptionNames);
            }
        }

        return null;
    }

    /**
     * Returns whether the given string contains a numeric reference to a wild card ("<n>").
     */
    private static boolean containsWildCardReferences(String string) {
        int openIndex = string.indexOf('<');
        if (openIndex < 0) {
            return false;
        }

        int closeIndex = string.indexOf('>', openIndex + 1);
        if (closeIndex < 0) {
            return false;
        }

        try {
            Integer.parseInt(string.substring(openIndex + 1, closeIndex));
        } catch (NumberFormatException e) {
            return false;
        }

        return true;
    }
}
